<?php

namespace Vier\Ethtool;

use Elliptic\EC;
use kornrunner\Keccak;

define("AddressPrefix","19");
define("NormalType",'57');
define("ContractType",'58');
define("AddressStringLength", 35);
define("AddressLength", 26);
define("KeyCurrentVersion", 4);
define("KeyVersion3", 3);

class KeyStore{

	// 保存到文件
	static function save($privateKey,$password,$dir){
		$address = self::privateToAddress($privateKey);
		$opts = [];
		try{
				$salt = isset($opts['salt']) ? $opts['salt'] : random_bytes(32);
				$iv = isset($opts['iv']) ? $opts['iv'] : random_bytes(16);
		} catch (\Exception $e){
				throw $e;
		}
		$kdf = isset($opts['kdf']) ? $opts['kdf'] : "scrypt";
		$kdfparams = array(
				"dklen" => isset($opts['dklen']) ? $opts['dklen'] : 32,
				'salt' => bin2hex($salt),
		);
		if($kdf === 'pbkdf2'){
				$kdfparams['c'] = isset($opts['c']) ? $opts['c'] : 262144;
				$kdfparams['prf'] = 'hmac-sha256';
				$derivedKey = hash_pbkdf2("sha256", $password, $salt, $kdfparams['c'], $kdfparams['dklen'] * 2, false );
		}else if($kdf = 'scrypt'){
				$kdfparams['n'] = isset($opts['n']) ? $opts['n'] : 4096;
				$kdfparams['r'] = isset($opts['r']) ? $opts['r'] : 8;
				$kdfparams['p'] = isset($opts['p']) ? $opts['p'] : 1;
				$derivedKey =  self::getScrypt($password, $salt , $kdfparams['n'],$kdfparams['r'],$kdfparams['p'],$kdfparams['dklen']);
		}else{
				throw new \Exception('Unsupported kdf');
		}
		$derivedKeyBin = hex2bin($derivedKey); //$derivedKey is a hex string
		$method = 'aes-128-ctr';
		$ciphertext = openssl_encrypt(hex2bin($privateKey), $method, substr($derivedKeyBin,0,16),$options=1 , $iv); //binary strinig
		$mac = hash("sha3-256", substr($derivedKeyBin,16,32) . $ciphertext . $iv . $method);
		try{
				$uuid = self::guidv4(random_bytes(16));
		}catch (\Exception $e){
				throw $e;
		}
		$json = array(
				"version" => KeyCurrentVersion,
				"id" => $uuid,
				"address" => $address,
				'crypto' => array(
						'ciphertext' => bin2hex($ciphertext),
						'cipherparams' => array(
								'iv' => bin2hex($iv),
						),
						'cipher' => $method,
						'kdf' => $kdf,
						'kdfparams' => $kdfparams,
						'mac' => $mac,
						'machash' => 'sha3256'
				),
		);

		$txt = json_encode($json);
		$wallet = $dir . '/' . substr($address,2) . '.json';
		file_put_contents($wallet,$txt);
		return $wallet;
	}

	// 加载凭证
	static function load($password,$wallet){
		$input = file_get_contents($wallet);

		$json = json_decode($input);
		if($json->version !== KeyVersion3 && $json->version !== KeyCurrentVersion)
				throw new \Exception('Not supported wallet version');
		if($json->crypto->kdf === 'scrypt'){
				$kdfparams = $json->crypto->kdfparams;
				$derivedKey =  self::getScrypt($password, hex2bin($kdfparams->salt) , $kdfparams->n,$kdfparams->r,$kdfparams->p,$kdfparams->dklen); //hex string
		}else if($json->crypto->kdf === 'pbkdf2'){
				$kdfparams = $json->crypto->kdfparams;
				$derivedKey = hash_pbkdf2("sha256", $password, hex2bin($kdfparams->salt), $kdfparams->c, $kdfparams-> dklen * 2, false );
		}else{
				throw new \Exception('Unsupported key derivation scheme');
		}
		$derivedKeyBin = hex2bin($derivedKey);
		$ciphertext = hex2bin($json->crypto->ciphertext);
		$method = $json->crypto->cipher;
		$iv = hex2bin($json->crypto->cipherparams->iv);
		if($json->version === KeyCurrentVersion){
				$mac = hash('sha3-256', substr($derivedKeyBin,16,32) . $ciphertext . $iv . $method );
		}else{
				$mac = hash('sha3-256', substr($derivedKeyBin,16,32) . $ciphertext);
		}
		if($mac !== $json->crypto->mac){
				throw new \Exception('Key derivation failed - possibly wrong passphrase');
		}
		$seed = openssl_decrypt($ciphertext, $method, substr($derivedKeyBin,0,16), $options=1, $iv);
		if(strlen($seed) < 32){
				$string = hex2bin("00000000"."00000000"."00000000"."00000000"."00000000"."00000000"."00000000"."00000000").$seed;
				$seed = substr($string,-32);
		}
		//echo "seed: ", bin2hex($seed) ,PHP_EOL;
		//$this->setPrivateKey(bin2hex($seed));
		return bin2hex($seed);
	}

	// 私钥转地址
	static function privateToAddress($priv_hex){
		$ec = new EC('secp256k1');
		$keyPair = $ec->keyFromPrivate($priv_hex);
		$public = $keyPair->getPublic()->encode('hex');
		return "0x" . substr(Keccak::hash(substr(hex2bin($public), 1), 256), 24);
	}

	static function  guidv4($data){
		assert(strlen($data) == 16);
		$data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100
		$data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10
		return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
	}

	static function getScrypt($password, $salt, $N, $r, $p, $kdlen){
		if ($N == 0 || ($N & ($N - 1)) != 0) {
				throw new \InvalidArgumentException("N must be > 0 and a power of 2");
		}
		if ($N > PHP_INT_MAX / 128 / $r) {
				throw new \InvalidArgumentException("Parameter N is too large");
		}
		if ($r > PHP_INT_MAX / 128 / $p) {
				throw new \InvalidArgumentException("Parameter r is too large");
		}
		return  scrypt($password, $salt, $N, $r, $p, $kdlen);
	}

}

